//  HTTPRequest

#ifndef HTTPREQUEST_HPP
#define HTTPREQUEST_HPP

#include <algorithm>
#include <array>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <system_error>
#include <type_traits>
#include <vector>

#if defined(_WIN32) || defined(__CYGWIN__)
#pragma push_macro("WIN32_LEAN_AND_MEAN")
#pragma push_macro("NOMINMAX")
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif  // WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif  // NOMINMAX
#include <winsock2.h>
#if _WIN32_WINNT < _WIN32_WINNT_WINXP
extern "C" char* _strdup(const char* strSource);
#define strdup _strdup
#include <wspiapi.h>
#endif  // _WIN32_WINNT < _WIN32_WINNT_WINXP
#include <ws2tcpip.h>
#pragma pop_macro("WIN32_LEAN_AND_MEAN")
#pragma pop_macro("NOMINMAX")
#else
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#endif  // defined(_WIN32) || defined(__CYGWIN__)

namespace http
{
class RequestError final : public std::logic_error
{
   public:
    using std::logic_error::logic_error;
};

class ResponseError final : public std::runtime_error
{
   public:
    using std::runtime_error::runtime_error;
};

enum class InternetProtocol : std::uint8_t
{
    V4,
    V6
};

struct Uri final
{
    std::string scheme;
    std::string user;
    std::string password;
    std::string host;
    std::string port;
    std::string path;
    std::string query;
    std::string fragment;
};

struct HttpVersion final
{
    uint16_t major;
    uint16_t minor;
};

struct Status final
{
    // RFC 7231, 6. Response Status Codes
    enum Code : std::uint16_t
    {
        Continue = 100,
        SwitchingProtocol = 101,
        Processing = 102,
        EarlyHints = 103,

        Ok = 200,
        Created = 201,
        Accepted = 202,
        NonAuthoritativeInformation = 203,
        NoContent = 204,
        ResetContent = 205,
        PartialContent = 206,
        MultiStatus = 207,
        AlreadyReported = 208,
        ImUsed = 226,

        MultipleChoice = 300,
        MovedPermanently = 301,
        Found = 302,
        SeeOther = 303,
        NotModified = 304,
        UseProxy = 305,
        TemporaryRedirect = 307,
        PermanentRedirect = 308,

        BadRequest = 400,
        Unauthorized = 401,
        PaymentRequired = 402,
        Forbidden = 403,
        NotFound = 404,
        MethodNotAllowed = 405,
        NotAcceptable = 406,
        ProxyAuthenticationRequired = 407,
        RequestTimeout = 408,
        Conflict = 409,
        Gone = 410,
        LengthRequired = 411,
        PreconditionFailed = 412,
        PayloadTooLarge = 413,
        UriTooLong = 414,
        UnsupportedMediaType = 415,
        RangeNotSatisfiable = 416,
        ExpectationFailed = 417,
        MisdirectedRequest = 421,
        UnprocessableEntity = 422,
        Locked = 423,
        FailedDependency = 424,
        TooEarly = 425,
        UpgradeRequired = 426,
        PreconditionRequired = 428,
        TooManyRequests = 429,
        RequestHeaderFieldsTooLarge = 431,
        UnavailableForLegalReasons = 451,

        InternalServerError = 500,
        NotImplemented = 501,
        BadGateway = 502,
        ServiceUnavailable = 503,
        GatewayTimeout = 504,
        HttpVersionNotSupported = 505,
        VariantAlsoNegotiates = 506,
        InsufficientStorage = 507,
        LoopDetected = 508,
        NotExtended = 510,
        NetworkAuthenticationRequired = 511
    };

    HttpVersion httpVersion;
    std::uint16_t code;
    std::string reason;
};

using HeaderField = std::pair<std::string, std::string>;
using HeaderFields = std::vector<HeaderField>;

struct Response final
{
    Status status;
    HeaderFields headerFields;
    std::vector<std::uint8_t> body;
};

inline namespace detail
{
#if defined(_WIN32) || defined(__CYGWIN__)
class WinSock final
{
   public:
    WinSock()
    {
        WSADATA wsaData;
        const auto error = WSAStartup(MAKEWORD(2, 2), &wsaData);
        if (error != 0) throw std::system_error{error, std::system_category(), "WSAStartup failed"};

        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
        {
            WSACleanup();
            throw std::runtime_error{"Invalid WinSock version"};
        }

        started = true;
    }

    ~WinSock()
    {
        if (started) WSACleanup();
    }

    WinSock(WinSock&& other) noexcept : started{other.started} { other.started = false; }

    WinSock& operator=(WinSock&& other) noexcept
    {
        if (&other == this) return *this;
        if (started) WSACleanup();
        started = other.started;
        other.started = false;
        return *this;
    }

   private:
    bool started = false;
};
#endif  // defined(_WIN32) || defined(__CYGWIN__)

inline int getLastError() noexcept
{
#if defined(_WIN32) || defined(__CYGWIN__)
    return WSAGetLastError();
#else
    return errno;
#endif  // defined(_WIN32) || defined(__CYGWIN__)
}

constexpr int getAddressFamily(const InternetProtocol internetProtocol)
{
    return (internetProtocol == InternetProtocol::V4) ? AF_INET
           : (internetProtocol == InternetProtocol::V6)
               ? AF_INET6
               : throw RequestError{"Unsupported protocol"};
}

class Socket final
{
   public:
#if defined(_WIN32) || defined(__CYGWIN__)
    using Type = SOCKET;
    static constexpr Type invalid = INVALID_SOCKET;
#else
    using Type = int;
    static constexpr Type invalid = -1;
#endif  // defined(_WIN32) || defined(__CYGWIN__)

    explicit Socket(const InternetProtocol internetProtocol)
        : endpoint{socket(getAddressFamily(internetProtocol), SOCK_STREAM, IPPROTO_TCP)}
    {
        if (endpoint == invalid)
            throw std::system_error{getLastError(), std::system_category(),
                                    "Failed to create socket"};

#if defined(_WIN32) || defined(__CYGWIN__)
        ULONG mode = 1;
        if (ioctlsocket(endpoint, FIONBIO, &mode) != 0)
        {
            close();
            throw std::system_error{WSAGetLastError(), std::system_category(),
                                    "Failed to get socket flags"};
        }
#else
        const auto flags = fcntl(endpoint, F_GETFL);
        if (flags == -1)
        {
            close();
            throw std::system_error{errno, std::system_category(), "Failed to get socket flags"};
        }

        if (fcntl(endpoint, F_SETFL, flags | O_NONBLOCK) == -1)
        {
            close();
            throw std::system_error{errno, std::system_category(), "Failed to set socket flags"};
        }
#endif  // defined(_WIN32) || defined(__CYGWIN__)

#ifdef __APPLE__
        const int value = 1;
        if (setsockopt(endpoint, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value)) == -1)
        {
            close();
            throw std::system_error{errno, std::system_category(), "Failed to set socket option"};
        }
#endif  // __APPLE__
    }

    ~Socket()
    {
        if (endpoint != invalid) close();
    }

    Socket(Socket&& other) noexcept : endpoint{other.endpoint} { other.endpoint = invalid; }

    Socket& operator=(Socket&& other) noexcept
    {
        if (&other == this) return *this;
        if (endpoint != invalid) close();
        endpoint = other.endpoint;
        other.endpoint = invalid;
        return *this;
    }

    void connect(const struct sockaddr* address, const socklen_t addressSize,
                 const std::int64_t timeout)
    {
#if defined(_WIN32) || defined(__CYGWIN__)
        auto result = ::connect(endpoint, address, addressSize);
        while (result == -1 && WSAGetLastError() == WSAEINTR)
            result = ::connect(endpoint, address, addressSize);

        if (result == -1)
        {
            if (WSAGetLastError() == WSAEWOULDBLOCK)
            {
                select(SelectType::write, timeout);

                char socketErrorPointer[sizeof(int)];
                socklen_t optionLength = sizeof(socketErrorPointer);
                if (getsockopt(endpoint, SOL_SOCKET, SO_ERROR, socketErrorPointer, &optionLength) ==
                    -1)
                    throw std::system_error{WSAGetLastError(), std::system_category(),
                                            "Failed to get socket option"};

                int socketError;
                std::memcpy(&socketError, socketErrorPointer, sizeof(socketErrorPointer));

                if (socketError != 0)
                    throw std::system_error{socketError, std::system_category(),
                                            "Failed to connect"};
            }
            else
                throw std::system_error{WSAGetLastError(), std::system_category(),
                                        "Failed to connect"};
        }
#else
        auto result = ::connect(endpoint, address, addressSize);
        while (result == -1 && errno == EINTR) result = ::connect(endpoint, address, addressSize);

        if (result == -1)
        {
            if (errno == EINPROGRESS)
            {
                select(SelectType::write, timeout);

                int socketError;
                socklen_t optionLength = sizeof(socketError);
                if (getsockopt(endpoint, SOL_SOCKET, SO_ERROR, &socketError, &optionLength) == -1)
                    throw std::system_error{errno, std::system_category(),
                                            "Failed to get socket option"};

                if (socketError != 0)
                    throw std::system_error{socketError, std::system_category(),
                                            "Failed to connect"};
            }
            else
                throw std::system_error{errno, std::system_category(), "Failed to connect"};
        }
#endif  // defined(_WIN32) || defined(__CYGWIN__)
    }

    std::size_t send(const void* buffer, const std::size_t length, const std::int64_t timeout)
    {
        select(SelectType::write, timeout);
#if defined(_WIN32) || defined(__CYGWIN__)
        auto result =
            ::send(endpoint, reinterpret_cast<const char*>(buffer), static_cast<int>(length), 0);

        while (result == -1 && WSAGetLastError() == WSAEINTR)
            result = ::send(endpoint, reinterpret_cast<const char*>(buffer),
                            static_cast<int>(length), 0);

        if (result == -1)
            throw std::system_error{WSAGetLastError(), std::system_category(),
                                    "Failed to send data"};
#else
        auto result = ::send(endpoint, reinterpret_cast<const char*>(buffer), length, noSignal);

        while (result == -1 && errno == EINTR)
            result = ::send(endpoint, reinterpret_cast<const char*>(buffer), length, noSignal);

        if (result == -1)
            throw std::system_error{errno, std::system_category(), "Failed to send data"};
#endif  // defined(_WIN32) || defined(__CYGWIN__)
        return static_cast<std::size_t>(result);
    }

    std::size_t recv(void* buffer, const std::size_t length, const std::int64_t timeout)
    {
        select(SelectType::read, timeout);
#if defined(_WIN32) || defined(__CYGWIN__)
        auto result =
            ::recv(endpoint, reinterpret_cast<char*>(buffer), static_cast<int>(length), 0);

        while (result == -1 && WSAGetLastError() == WSAEINTR)
            result = ::recv(endpoint, reinterpret_cast<char*>(buffer), static_cast<int>(length), 0);

        if (result == -1)
            throw std::system_error{WSAGetLastError(), std::system_category(),
                                    "Failed to read data"};
#else
        auto result = ::recv(endpoint, reinterpret_cast<char*>(buffer), length, noSignal);

        while (result == -1 && errno == EINTR)
            result = ::recv(endpoint, reinterpret_cast<char*>(buffer), length, noSignal);

        if (result == -1)
            throw std::system_error{errno, std::system_category(), "Failed to read data"};
#endif  // defined(_WIN32) || defined(__CYGWIN__)
        return static_cast<std::size_t>(result);
    }

   private:
    enum class SelectType
    {
        read,
        write
    };

    void select(const SelectType type, const std::int64_t timeout)
    {
        fd_set descriptorSet;
        FD_ZERO(&descriptorSet);
        FD_SET(endpoint, &descriptorSet);

#if defined(_WIN32) || defined(__CYGWIN__)
        TIMEVAL selectTimeout{static_cast<LONG>(timeout / 1000),
                              static_cast<LONG>((timeout % 1000) * 1000)};
        auto count = ::select(0, (type == SelectType::read) ? &descriptorSet : nullptr,
                              (type == SelectType::write) ? &descriptorSet : nullptr, nullptr,
                              (timeout >= 0) ? &selectTimeout : nullptr);

        while (count == -1 && WSAGetLastError() == WSAEINTR)
            count = ::select(0, (type == SelectType::read) ? &descriptorSet : nullptr,
                             (type == SelectType::write) ? &descriptorSet : nullptr, nullptr,
                             (timeout >= 0) ? &selectTimeout : nullptr);

        if (count == -1)
            throw std::system_error{WSAGetLastError(), std::system_category(),
                                    "Failed to select socket"};
        else if (count == 0)
            throw ResponseError{"Request timed out"};
#else
        timeval selectTimeout{static_cast<time_t>(timeout / 1000),
                              static_cast<suseconds_t>((timeout % 1000) * 1000)};
        auto count = ::select(endpoint + 1, (type == SelectType::read) ? &descriptorSet : nullptr,
                              (type == SelectType::write) ? &descriptorSet : nullptr, nullptr,
                              (timeout >= 0) ? &selectTimeout : nullptr);

        while (count == -1 && errno == EINTR)
            count = ::select(endpoint + 1, (type == SelectType::read) ? &descriptorSet : nullptr,
                             (type == SelectType::write) ? &descriptorSet : nullptr, nullptr,
                             (timeout >= 0) ? &selectTimeout : nullptr);

        if (count == -1)
            throw std::system_error{errno, std::system_category(), "Failed to select socket"};
        else if (count == 0)
            throw ResponseError{"Request timed out"};
#endif  // defined(_WIN32) || defined(__CYGWIN__)
    }

    void close() noexcept
    {
#if defined(_WIN32) || defined(__CYGWIN__)
        closesocket(endpoint);
#else
        ::close(endpoint);
#endif  // defined(_WIN32) || defined(__CYGWIN__)
    }

#if defined(__unix__) && !defined(__APPLE__) && !defined(__CYGWIN__)
    static constexpr int noSignal = MSG_NOSIGNAL;
#else
    static constexpr int noSignal = 0;
#endif  // defined(__unix__) && !defined(__APPLE__)

    Type endpoint = invalid;
};

// RFC 7230, 3.2.3. WhiteSpace
template <typename C>
constexpr bool isWhiteSpaceChar(const C c) noexcept
{
    return c == 0x20 || c == 0x09;  // space or tab
};

// RFC 5234, Appendix B.1. Core Rules
template <typename C>
constexpr bool isDigitChar(const C c) noexcept
{
    return c >= 0x30 && c <= 0x39;  // 0 - 9
}

// RFC 5234, Appendix B.1. Core Rules
template <typename C>
constexpr bool isAlphaChar(const C c) noexcept
{
    return (c >= 0x61 && c <= 0x7A) ||  // a - z
           (c >= 0x41 && c <= 0x5A);    // A - Z
}

// RFC 7230, 3.2.6. Field Value Components
template <typename C>
constexpr bool isTokenChar(const C c) noexcept
{
    return c == 0x21 ||  // !
           c == 0x23 ||  // #
           c == 0x24 ||  // $
           c == 0x25 ||  // %
           c == 0x26 ||  // &
           c == 0x27 ||  // '
           c == 0x2A ||  // *
           c == 0x2B ||  // +
           c == 0x2D ||  // -
           c == 0x2E ||  // .
           c == 0x5E ||  // ^
           c == 0x5F ||  // _
           c == 0x60 ||  // `
           c == 0x7C ||  // |
           c == 0x7E ||  // ~
           isDigitChar(c) || isAlphaChar(c);
};
// RFC 5234, Appendix B.1. Core Rules
template <typename C>
constexpr bool isVisibleChar(const C c) noexcept
{
    return c >= 0x21 && c <= 0x7E;
}
// RFC 7230, Appendix B. Collected ABNF
template <typename C>
constexpr bool isObsoleteTextChar(const C c) noexcept
{
    return static_cast<unsigned char>(c) >= 0x80 && static_cast<unsigned char>(c) <= 0xFF;
}
template <class Iterator>
Iterator skipWhiteSpaces(const Iterator begin, const Iterator end)
{
    auto i = begin;
    for (i = begin; i != end; ++i)
        if (!isWhiteSpaceChar(*i)) break;
    return i;
}
// RFC 5234, Appendix B.1. Core Rules
template <typename T, typename C,
          typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
constexpr T digitToUint(const C c)
{
    // DIGIT
    return (c >= 0x30 && c <= 0x39) ? static_cast<T>(c - 0x30) :  // 0 - 9
               throw ResponseError{"Invalid digit"};
}
// RFC 5234, Appendix B.1. Core Rules
template <typename T, typename C,
          typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
constexpr T hexDigitToUint(const C c)
{
    // HEXDIG
    return (c >= 0x30 && c <= 0x39) ? static_cast<T>(c - 0x30) :  // 0 - 9
               (c >= 0x41 && c <= 0x46) ? static_cast<T>(c - 0x41) + T(10)
                                        :  // A - Z
               (c >= 0x61 && c <= 0x66) ? static_cast<T>(c - 0x61) + T(10)
                                        :  // a - z, some services send lower-case hex digits
               throw ResponseError{"Invalid hex digit"};
}
// RFC 3986, 3. Syntax Components
template <class Iterator>
Uri parseUri(const Iterator begin, const Iterator end)
{
    Uri result;
    // RFC 3986, 3.1. Scheme
    auto i = begin;
    if (i == end || !isAlphaChar(*begin)) throw RequestError{"Invalid scheme"};
    result.scheme.push_back(*i++);
    for (; i != end && (isAlphaChar(*i) || isDigitChar(*i) || *i == '+' || *i == '-' || *i == '.');
         ++i)
        result.scheme.push_back(*i);
    if (i == end || *i++ != ':') throw RequestError{"Invalid scheme"};
    if (i == end || *i++ != '/') throw RequestError{"Invalid scheme"};
    if (i == end || *i++ != '/') throw RequestError{"Invalid scheme"};
    // RFC 3986, 3.2. Authority
    std::string authority = std::string(i, end);
    // RFC 3986, 3.5. Fragment
    const auto fragmentPosition = authority.find('#');
    if (fragmentPosition != std::string::npos)
    {
        result.fragment = authority.substr(fragmentPosition + 1);
        authority.resize(fragmentPosition);  // remove the fragment part
    }
    // RFC 3986, 3.4. Query
    const auto queryPosition = authority.find('?');
    if (queryPosition != std::string::npos)
    {
        result.query = authority.substr(queryPosition + 1);
        authority.resize(queryPosition);  // remove the query part
    }
    // RFC 3986, 3.3. Path
    const auto pathPosition = authority.find('/');
    if (pathPosition != std::string::npos)
    {
        // RFC 3986, 3.3. Path
        result.path = authority.substr(pathPosition);
        authority.resize(pathPosition);
    }
    else
        result.path = "/";
    // RFC 3986, 3.2.1. User Information
    std::string userinfo;
    const auto hostPosition = authority.find('@');
    if (hostPosition != std::string::npos)
    {
        userinfo = authority.substr(0, hostPosition);
        const auto passwordPosition = userinfo.find(':');
        if (passwordPosition != std::string::npos)
        {
            result.user = userinfo.substr(0, passwordPosition);
            result.password = userinfo.substr(passwordPosition + 1);
        }
        else
            result.user = userinfo;
        result.host = authority.substr(hostPosition + 1);
    }
    else
        result.host = authority;
    // RFC 3986, 3.2.2. Host
    const auto portPosition = result.host.find(':');
    if (portPosition != std::string::npos)
    {
        // RFC 3986, 3.2.3. Port
        result.port = result.host.substr(portPosition + 1);
        result.host.resize(portPosition);
    }
    return result;
}
// RFC 7230, 2.6. Protocol Versioning
template <class Iterator>
std::pair<Iterator, HttpVersion> parseHttpVersion(const Iterator begin, const Iterator end)
{
    auto i = begin;
    if (i == end || *i++ != 'H') throw ResponseError{"Invalid HTTP version"};
    if (i == end || *i++ != 'T') throw ResponseError{"Invalid HTTP version"};
    if (i == end || *i++ != 'T') throw ResponseError{"Invalid HTTP version"};
    if (i == end || *i++ != 'P') throw ResponseError{"Invalid HTTP version"};
    if (i == end || *i++ != '/') throw ResponseError{"Invalid HTTP version"};
    if (i == end) throw ResponseError{"Invalid HTTP version"};
    const auto majorVersion = digitToUint<std::uint16_t>(*i++);
    if (i == end || *i++ != '.') throw ResponseError{"Invalid HTTP version"};
    if (i == end) throw ResponseError{"Invalid HTTP version"};
    const auto minorVersion = digitToUint<std::uint16_t>(*i++);
    return {i, HttpVersion{majorVersion, minorVersion}};
}
// RFC 7230, 3.1.2. Status Line
template <class Iterator>
std::pair<Iterator, std::uint16_t> parseStatusCode(const Iterator begin, const Iterator end)
{
    std::uint16_t result = 0;
    auto i = begin;
    while (i != end && isDigitChar(*i))
        result = static_cast<std::uint16_t>(result * 10U) + digitToUint<std::uint16_t>(*i++);
    if (std::distance(begin, i) != 3) throw ResponseError{"Invalid status code"};
    return {i, result};
}
// RFC 7230, 3.1.2. Status Line
template <class Iterator>
std::pair<Iterator, std::string> parseReasonPhrase(const Iterator begin, const Iterator end)
{
    std::string result;
    auto i = begin;
    for (; i != end && (isWhiteSpaceChar(*i) || isVisibleChar(*i) || isObsoleteTextChar(*i)); ++i)
        result.push_back(static_cast<char>(*i));
    return {i, std::move(result)};
}
// RFC 7230, 3.2.6. Field Value Components
template <class Iterator>
std::pair<Iterator, std::string> parseToken(const Iterator begin, const Iterator end)
{
    std::string result;
    auto i = begin;
    for (; i != end && isTokenChar(*i); ++i) result.push_back(static_cast<char>(*i));
    if (result.empty()) throw ResponseError{"Invalid token"};
    return {i, std::move(result)};
}
// RFC 7230, 3.2. Header Fields
template <class Iterator>
std::pair<Iterator, std::string> parseFieldValue(const Iterator begin, const Iterator end)
{
    std::string result;
    auto i = begin;
    for (; i != end && (isWhiteSpaceChar(*i) || isVisibleChar(*i) || isObsoleteTextChar(*i)); ++i)
        result.push_back(static_cast<char>(*i));
    // trim white spaces
    result.erase(std::find_if(result.rbegin(), result.rend(),
                              [](const char c) noexcept { return !isWhiteSpaceChar(c); })
                     .base(),
                 result.end());
    return {i, std::move(result)};
}
// RFC 7230, 3.2. Header Fields
template <class Iterator>
std::pair<Iterator, std::string> parseFieldContent(const Iterator begin, const Iterator end)
{
    std::string result;
    auto i = begin;
    for (;;)
    {
        const auto fieldValueResult = parseFieldValue(i, end);
        i = fieldValueResult.first;
        result += fieldValueResult.second;
        // Handle obsolete fold as per RFC 7230, 3.2.4. Field Parsing
        // Obsolete folding is known as linear white space (LWS) in RFC 2616, 2.2 Basic Rules
        auto obsoleteFoldIterator = i;
        if (obsoleteFoldIterator == end || *obsoleteFoldIterator++ != '\r') break;
        if (obsoleteFoldIterator == end || *obsoleteFoldIterator++ != '\n') break;
        if (obsoleteFoldIterator == end || !isWhiteSpaceChar(*obsoleteFoldIterator++)) break;
        result.push_back(' ');
        i = obsoleteFoldIterator;
    }
    return {i, std::move(result)};
}
// RFC 7230, 3.2. Header Fields
template <class Iterator>
std::pair<Iterator, HeaderField> parseHeaderField(const Iterator begin, const Iterator end)
{
    auto tokenResult = parseToken(begin, end);
    auto i = tokenResult.first;
    auto fieldName = std::move(tokenResult.second);
    if (i == end || *i++ != ':') throw ResponseError{"Invalid header"};
    i = skipWhiteSpaces(i, end);
    auto valueResult = parseFieldContent(i, end);
    i = valueResult.first;
    auto fieldValue = std::move(valueResult.second);
    if (i == end || *i++ != '\r') throw ResponseError{"Invalid header"};
    if (i == end || *i++ != '\n') throw ResponseError{"Invalid header"};
    return {i, {std::move(fieldName), std::move(fieldValue)}};
}
// RFC 7230, 3.1.2. Status Line
template <class Iterator>
std::pair<Iterator, Status> parseStatusLine(const Iterator begin, const Iterator end)
{
    const auto httpVersionResult = parseHttpVersion(begin, end);
    auto i = httpVersionResult.first;
    if (i == end || *i++ != ' ') throw ResponseError{"Invalid status line"};
    const auto statusCodeResult = parseStatusCode(i, end);
    i = statusCodeResult.first;
    if (i == end || *i++ != ' ') throw ResponseError{"Invalid status line"};
    auto reasonPhraseResult = parseReasonPhrase(i, end);
    i = reasonPhraseResult.first;
    if (i == end || *i++ != '\r') throw ResponseError{"Invalid status line"};
    if (i == end || *i++ != '\n') throw ResponseError{"Invalid status line"};
    return {i, Status{httpVersionResult.second, statusCodeResult.second,
                      std::move(reasonPhraseResult.second)}};
}
// RFC 7230, 4.1. Chunked Transfer Coding
template <typename T, class Iterator,
          typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
T stringToUint(const Iterator begin, const Iterator end)
{
    T result = 0;
    for (auto i = begin; i != end; ++i) result = T(10U) * result + digitToUint<T>(*i);
    return result;
}
template <typename T, class Iterator,
          typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
T hexStringToUint(const Iterator begin, const Iterator end)
{
    T result = 0;
    for (auto i = begin; i != end; ++i) result = T(16U) * result + hexDigitToUint<T>(*i);
    return result;
}
// RFC 7230, 3.1.1. Request Line
inline std::string encodeRequestLine(const std::string& method, const std::string& target)
{
    return method + " " + target + " HTTP/1.1\r\n";
}
// RFC 7230, 3.2. Header Fields
inline std::string encodeHeaderFields(const HeaderFields& headerFields)
{
    std::string result;
    for (const auto& headerField : headerFields)
    {
        if (headerField.first.empty()) throw RequestError{"Invalid header field name"};
        for (const auto c : headerField.first)
            if (!isTokenChar(c)) throw RequestError{"Invalid header field name"};
        for (const auto c : headerField.second)
            if (!isWhiteSpaceChar(c) && !isVisibleChar(c) && !isObsoleteTextChar(c))
                throw RequestError{"Invalid header field value"};
        result += headerField.first + ": " + headerField.second + "\r\n";
    }
    return result;
}
// RFC 4648, 4. Base 64 Encoding
template <class Iterator>
std::string encodeBase64(const Iterator begin, const Iterator end)
{
    constexpr std::array<char, 64> chars{
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
        'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
    std::string result;
    std::size_t c = 0;
    std::array<std::uint8_t, 3> charArray;
    for (auto i = begin; i != end; ++i)
    {
        charArray[c++] = static_cast<std::uint8_t>(*i);
        if (c == 3)
        {
            result += chars[static_cast<std::uint8_t>((charArray[0] & 0xFC) >> 2)];
            result += chars[static_cast<std::uint8_t>(((charArray[0] & 0x03) << 4) +
                                                      ((charArray[1] & 0xF0) >> 4))];
            result += chars[static_cast<std::uint8_t>(((charArray[1] & 0x0F) << 2) +
                                                      ((charArray[2] & 0xC0) >> 6))];
            result += chars[static_cast<std::uint8_t>(charArray[2] & 0x3f)];
            c = 0;
        }
    }
    if (c)
    {
        result += chars[static_cast<std::uint8_t>((charArray[0] & 0xFC) >> 2)];
        if (c == 1)
            result += chars[static_cast<std::uint8_t>((charArray[0] & 0x03) << 4)];
        else  // c == 2
        {
            result += chars[static_cast<std::uint8_t>(((charArray[0] & 0x03) << 4) +
                                                      ((charArray[1] & 0xF0) >> 4))];
            result += chars[static_cast<std::uint8_t>((charArray[1] & 0x0F) << 2)];
        }
        while (++c < 4) result += '=';  // padding
    }
    return result;
}
inline std::vector<std::uint8_t> encodeHtml(const Uri& uri, const std::string& method,
                                            const std::vector<uint8_t>& body,
                                            HeaderFields headerFields)
{
    if (uri.scheme != "http") throw RequestError{"Only HTTP scheme is supported"};
    // RFC 7230, 5.3. Request Target
    const std::string requestTarget = uri.path + (uri.query.empty() ? "" : '?' + uri.query);
    // RFC 7230, 5.4. Host
    headerFields.push_back({"Host", uri.host});
    // RFC 7230, 3.3.2. Content-Length
    headerFields.push_back({"Content-Length", std::to_string(body.size())});
    // RFC 7617, 2. The 'Basic' Authentication Scheme
    if (!uri.user.empty() || !uri.password.empty())
    {
        std::string userinfo = uri.user + ':' + uri.password;
        headerFields.push_back(
            {"Authorization", "Basic " + encodeBase64(userinfo.begin(), userinfo.end())});
    }
    const auto headerData =
        encodeRequestLine(method, requestTarget) + encodeHeaderFields(headerFields) + "\r\n";
    std::vector<uint8_t> result(headerData.begin(), headerData.end());
    result.insert(result.end(), body.begin(), body.end());
    return result;
}
}  // namespace detail
class Request final
{
   public:
    explicit Request(const std::string& uriString,
                     const InternetProtocol protocol = InternetProtocol::V4)
        : internetProtocol{protocol}, uri{parseUri(uriString.begin(), uriString.end())}
    {
    }
    Response send(const std::string& method = "GET", const std::string& body = "",
                  const HeaderFields& headerFields = {},
                  const std::chrono::milliseconds timeout = std::chrono::milliseconds{-1})
    {
        return send(method, std::vector<uint8_t>(body.begin(), body.end()), headerFields, timeout);
    }
    Response send(const std::string& method, const std::vector<uint8_t>& body,
                  const HeaderFields& headerFields = {},
                  const std::chrono::milliseconds timeout = std::chrono::milliseconds{-1})
    {
        const auto stopTime = std::chrono::steady_clock::now() + timeout;
        if (uri.scheme != "http") throw RequestError{"Only HTTP scheme is supported"};
        addrinfo hints = {};
        hints.ai_family = getAddressFamily(internetProtocol);
        hints.ai_socktype = SOCK_STREAM;
        const char* port = uri.port.empty() ? "80" : uri.port.c_str();
        addrinfo* info;
        if (getaddrinfo(uri.host.c_str(), port, &hints, &info) != 0)
            throw std::system_error{getLastError(), std::system_category(),
                                    "Failed to get address info of " + uri.host};
        const std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> addressInfo{info, freeaddrinfo};
        const auto requestData = encodeHtml(uri, method, body, headerFields);
        Socket socket{internetProtocol};
        const auto getRemainingMilliseconds =
            [](const std::chrono::steady_clock::time_point time) noexcept -> std::int64_t
        {
            const auto now = std::chrono::steady_clock::now();
            const auto remainingTime =
                std::chrono::duration_cast<std::chrono::milliseconds>(time - now);
            return (remainingTime.count() > 0) ? remainingTime.count() : 0;
        };
        // take the first address from the list
        socket.connect(addressInfo->ai_addr, static_cast<socklen_t>(addressInfo->ai_addrlen),
                       (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1);
        auto remaining = requestData.size();
        auto sendData = requestData.data();
        // send the request
        while (remaining > 0)
        {
            const auto size =
                socket.send(sendData, remaining,
                            (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1);
            remaining -= size;
            sendData += size;
        }
        std::array<std::uint8_t, 4096> tempBuffer;
        constexpr std::array<std::uint8_t, 2> crlf = {'\r', '\n'};
        constexpr std::array<std::uint8_t, 4> headerEnd = {'\r', '\n', '\r', '\n'};
        Response response;
        std::vector<std::uint8_t> responseData;
        bool parsingBody = false;
        bool contentLengthReceived = false;
        std::size_t contentLength = 0U;
        bool chunkedResponse = false;
        std::size_t expectedChunkSize = 0U;
        bool removeCrlfAfterChunk = false;
        // read the response
        for (;;)
        {
            const auto size =
                socket.recv(tempBuffer.data(), tempBuffer.size(),
                            (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1);
            if (size == 0)  // disconnected
                return response;
            responseData.insert(responseData.end(), tempBuffer.begin(), tempBuffer.begin() + size);
            if (!parsingBody)
            {
                // RFC 7230, 3. Message Format
                // Empty line indicates the end of the header section (RFC 7230, 2.1. Client/Server
                // Messaging)
                const auto endIterator = std::search(responseData.cbegin(), responseData.cend(),
                                                     headerEnd.cbegin(), headerEnd.cend());
                if (endIterator == responseData.cend()) break;  // two consecutive CRLFs not found
                const auto headerBeginIterator = responseData.cbegin();
                const auto headerEndIterator = endIterator + 2;
                auto statusLineResult = parseStatusLine(headerBeginIterator, headerEndIterator);
                auto i = statusLineResult.first;
                response.status = std::move(statusLineResult.second);
                for (;;)
                {
                    auto headerFieldResult = parseHeaderField(i, headerEndIterator);
                    i = headerFieldResult.first;
                    auto fieldName = std::move(headerFieldResult.second.first);
                    const auto toLower = [](const char c) noexcept
                    { return (c >= 'A' && c <= 'Z') ? c - ('A' - 'a') : c; };
                    std::transform(fieldName.begin(), fieldName.end(), fieldName.begin(), toLower);
                    auto fieldValue = std::move(headerFieldResult.second.second);
                    if (fieldName == "transfer-encoding")
                    {
                        // RFC 7230, 3.3.1. Transfer-Encoding
                        if (fieldValue == "chunked")
                            chunkedResponse = true;
                        else
                            throw ResponseError{"Unsupported transfer encoding: " + fieldValue};
                    }
                    else if (fieldName == "content-length")
                    {
                        // RFC 7230, 3.3.2. Content-Length
                        contentLength =
                            stringToUint<std::size_t>(fieldValue.cbegin(), fieldValue.cend());
                        contentLengthReceived = true;
                        response.body.reserve(contentLength);
                    }
                    response.headerFields.push_back({std::move(fieldName), std::move(fieldValue)});
                    if (i == headerEndIterator) break;
                }
                responseData.erase(responseData.cbegin(), headerEndIterator + 2);
                parsingBody = true;
            }
            if (parsingBody)
            {
                // Content-Length must be ignored if Transfer-Encoding is received (RFC 7230, 3.2.
                // Content-Length)
                if (chunkedResponse)
                {
                    // RFC 7230, 4.1. Chunked Transfer Coding
                    for (;;)
                    {
                        if (expectedChunkSize > 0)
                        {
                            const auto toWrite = (std::min)(expectedChunkSize, responseData.size());
                            response.body.insert(
                                response.body.end(), responseData.begin(),
                                responseData.begin() + static_cast<std::ptrdiff_t>(toWrite));
                            responseData.erase(
                                responseData.begin(),
                                responseData.begin() + static_cast<std::ptrdiff_t>(toWrite));
                            expectedChunkSize -= toWrite;
                            if (expectedChunkSize == 0) removeCrlfAfterChunk = true;
                            if (responseData.empty()) break;
                        }
                        else
                        {
                            if (removeCrlfAfterChunk)
                            {
                                if (responseData.size() < 2) break;

                                if (!std::equal(crlf.begin(), crlf.end(), responseData.begin()))
                                    throw ResponseError{"Invalid chunk"};
                                removeCrlfAfterChunk = false;
                                responseData.erase(responseData.begin(), responseData.begin() + 2);
                            }
                            const auto i = std::search(responseData.begin(), responseData.end(),
                                                       crlf.begin(), crlf.end());
                            if (i == responseData.end()) break;
                            expectedChunkSize =
                                detail::hexStringToUint<std::size_t>(responseData.begin(), i);
                            responseData.erase(responseData.begin(), i + 2);
                            if (expectedChunkSize == 0) return response;
                        }
                    }
                }
                else
                {
                    response.body.insert(response.body.end(), responseData.begin(),
                                         responseData.end());
                    responseData.clear();
                    // got the whole content
                    if (contentLengthReceived && response.body.size() >= contentLength)
                        return response;
                }
            }
        }
        return response;
    }

   private:
#if defined(_WIN32) || defined(__CYGWIN__)
    WinSock winSock;
#endif  // defined(_WIN32) || defined(__CYGWIN__)
    InternetProtocol internetProtocol;
    Uri uri;
};
}  // namespace http
#endif  // HTTPREQUEST_HPP